/**
 * 
 */
package fr.utbm.gi.vi51.slizers.framework.environment;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.vecmath.Point2i;

import org.janusproject.kernel.address.AgentAddress;

import fr.utbm.gi.vi51.slizers.framework.agent.AbstractSituatedObject;
import fr.utbm.gi.vi51.slizers.framework.agent.AgentBody;
import fr.utbm.gi.vi51.slizers.framework.agent.Direction;
import fr.utbm.gi.vi51.slizers.framework.agent.SituatedAgent;
import fr.utbm.gi.vi51.slizers.framework.agent.SituatedObject;
import fr.utbm.gi.vi51.slizers.framework.schedule.SimulationTimeManager;
import fr.utbm.gi.vi51.slizers.framework.util.Vector2i;
import fr.utbm.gi.vi51.slizers.pw.agent.action.CreatureActionInfluence;

/**
 * Abstract implementation of an environment.
 *
 * @author Tarik Belabbas
 *
 */
public abstract class AbstractEnvironment implements Environment
{
	/**
	 * The bodies contained in the environment and their adresses.
	 */
	private final Map< AgentAddress, AgentBody >	bodies		= 
		new TreeMap< AgentAddress, AgentBody >();

	/**
	 * The simulation time manager of the environment.
	 */
	private final SimulationTimeManager				timeManager;

	/**
	 * The listeners of the environment.
	 */
	private final Collection< EnvironmentListener >	listeners	=
		new ArrayList< EnvironmentListener >();

	/**
	 * The influence collector of the environment.
	 */
	private final InfluenceCollector< ? extends Environment >		influenceCollector;

	/**
	 * The influence conflict solver of the environment.
	 */
	private final InfluenceConflictSolver< ? extends Environment >	influenceConflictSolver;

	/**
	 * The perception generator of the environment
	 */
	private final PerceptionGenerator< ? extends Environment >		perceptionGenerator;

	/**
	 * A boolean to notify environment changes.
	 */
	private final AtomicBoolean			changed		= new AtomicBoolean();

	/**
	 * A boolean to notify that the environment has been initialized (the run()
	 * function has been executed at least once).
	 */
	private final AtomicBoolean			init		= new AtomicBoolean( true );

	/**
	 * Parameterized constructor.
	 * 
	 * @param timeManager the time manager to use
	 */
	public AbstractEnvironment( SimulationTimeManager _timeManager )
	{
		this.timeManager	= _timeManager;
		
		this.influenceCollector			= createInfluenceCollector();
		this.influenceConflictSolver	= createInfluenceConflictSolver();
		this.perceptionGenerator		= createPerceptionGenerator();
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.Environment#run()
	 */
	@Override
	public void run()
	{
		if ( this.init.getAndSet( false ) )
		{
			notifyListeners( createStateEvent() );
		}

		// Collect influences
		Collection< CreatureActionInfluence > influences = collectInfluences();
		
		if ( !influences.isEmpty() )
		{
			this.changed.set( false );
			
			// Apply influences
			applyInfluences( influences, this.timeManager );
			
			if ( this.changed.get() )
			{
				notifyListeners( createStateEvent() );
			}
		}
		
		// Generate perceptions
		List< Perception > list;
		for ( AgentBody body : this.bodies.values() )
		{
			list = computePerceptionsFor( body );
			if ( list == null )
			{
				list = Collections.emptyList();
			}
			
			body.setPerceptions( list );
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * fr.utbm.gi.vi51.slizers.framework.environment.Environment#getAgentBodies
	 * ()
	 */
	@Override
	public Collection< AgentBody > getAgentBodies()
	{
		return Collections.unmodifiableCollection( this.bodies.values() );
	}
	
	/* (non-Javadoc)
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.Environment#getAgentBodyFor(org.janusproject.kernel.address.AgentAddress)
	 */
	@Override
	public AgentBody getAgentBodyFor( AgentAddress _agentAddress )
	{
		return this.bodies.get( _agentAddress );
	}	

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.Environment#
	 * addEnvironmentListener
	 * (fr.utbm.gi.vi51.slizers.framework.environment.EnvironmentListener)
	 */
	@Override
	public void addEnvironmentListener( EnvironmentListener _listener )
	{
		synchronized ( this.listeners )
		{
			this.listeners.add( _listener );
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see fr.utbm.gi.vi51.slizers.framework.environment.Environment#
	 * removeEnvironmentListener
	 * (fr.utbm.gi.vi51.slizers.framework.environment.EnvironmentListener)
	 */
	@Override
	public void removeEnvironmentListener( EnvironmentListener _listener )
	{
		synchronized ( this.listeners )
		{
			this.listeners.remove( _listener );
		}

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * fr.utbm.gi.vi51.slizers.framework.environment.Environment#spawnAgentBody
	 * (fr.utbm.gi.vi51.slizers.framework.environment.SituatedAgent)
	 */
	@Override
	public void spawnAgentBody( SituatedAgent< ? > _agent )
	{
		if ( _agent != null )
		{
			AgentBody body = _agent.spawnBody( this );
			if ( body != null )
			{
				Point2i startingPosition = getStartingPosition();
				body.setPosition( startingPosition );
				body.setDirection( Direction.random() );
				
				this.bodies.put( _agent.getAddress(), body );
			}
		}
	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * fr.utbm.gi.vi51.slizers.framework.environment.Environment#killAgentBody
	 * (org.janusproject.kernel.address.AgentAddress)
	 */
	@Override
	public void killAgentBody( AgentAddress _agentAddress )
	{
		this.bodies.remove( _agentAddress );
	}

	/**
	 * Clones the list of agents in the environment and returns it.
	 * 
	 * @return the clone of the list of agents
	 */
	protected Collection< AgentBody > cloneAgentBodies()
	{
		Collection< AgentBody > clone = new ArrayList< AgentBody >(	this.bodies.size() );
		clone.addAll( this.bodies.values() );
		
		return clone;
	}
	
	/**
	 * Notifies all environment listeners about an event.
	 * 
	 * @param _event the event
	 */
	protected void notifyListeners( EnvironmentEvent _event )
	{
		EnvironmentListener[] list;
		synchronized ( this.listeners )
		{
			list = new EnvironmentListener[ this.listeners.size() ];
			this.listeners.toArray( list );
		}
		
		for ( EnvironmentListener listener : list )
		{
			listener.environmentChanged( _event );
		}
	}
	
	/**
	 * Creates and returns an event about an environment change.
	 * 
	 * @return the environment event.
	 */
	protected abstract EnvironmentEvent createStateEvent();
	
	/**
	 * Creates an influence collector for the environment.
	 * 
	 * @return the influence collector
	 */
	protected abstract InfluenceCollector< ? extends Environment > createInfluenceCollector();
	
	/**
	 * Creates an influence conflict solver for the environment.
	 * 
	 * @return the influence conflict solver
	 */
	protected abstract InfluenceConflictSolver< ? extends Environment > createInfluenceConflictSolver();
	
	/**
	 * Creates an perception generator for the environment.
	 * 
	 * @return the perception generator
	 */
	protected abstract PerceptionGenerator< ? extends Environment > createPerceptionGenerator();
	
	/**
	 * Computes and returns the perceptions of an agent body.
	 * 
	 * @param _agent the body of the perceiver agent
	 * @return the perceptions of the agent body
	 */
	private List< Perception > computePerceptionsFor( AgentBody _agentBody )
	{
		return this.perceptionGenerator.computePerceptionsFor( _agentBody );
	}
	
	/**
	 * Collects and returns the influences of agent bodies in the environment.
	 * 
	 * @return the collected influences
	 */
	private Collection< CreatureActionInfluence > collectInfluences()
	{
		return this.influenceCollector.collectInfluences();
	}
	
	/**
	 * Solves influence conflicts and returns a list of influences without
	 * conflicts.
	 * 
	 * @param _influences the influences to solve
	 * @return the new influences, without conflicts
	 */
	private Collection< CreatureActionInfluence > solveInfluenceConflicts(
			Collection< CreatureActionInfluence > _influences )
	{
		return this.influenceConflictSolver.solveInfluenceConflicts( _influences );
	}

	/**
	 * Applies resulting actions of influences.
	 * 
	 * @param _influences the influences to apply
	 * @param _timeManager the time manager of the environment
	 */
	protected abstract void applyInfluences( Collection< CreatureActionInfluence > influences,
			SimulationTimeManager timeManager );
	
	/**
	 * Move the given object.
	 * 
	 * @param _object the object to move
	 * @param _instantTranslation the linear motion of the object
	 */
	protected final void move( SituatedObject _object, Vector2i _instantTranslation )
	{
		if ( _object instanceof AbstractSituatedObject )
		{
			AbstractSituatedObject object = ( AbstractSituatedObject ) _object;
			float duration = ( float )this.timeManager.getTimeStepDuration() / 1000.0f;
			
			object.move( _instantTranslation.getX(), _instantTranslation.getY(),
					duration, getWidth(), getHeight() );
			
			this.changed.set( true );
		}
		else
		{
			throw new IllegalArgumentException( "_object" ); //$NON-NLS-1$
		}
	}
	
	/**
	 * Returns the starting position of spawned bodies.
	 * 
	 * @return the starting position
	 */
	protected abstract Point2i getStartingPosition();
}
